Tic Tac Toe
Volume Number: 2
Issue Number: 10
Column Tag: Assembly Language Lab
Create a Tic Tac Toe Game!
By Mikeā„¢ Scanlin, World Traveler
While reading MacTutor and learning how to program in 68000, I decided to try
and write a simple game. I wanted something that was simple enough so that I could
concentrate on the coding of it rather than the logic of it. Tic Tac Toe was the obvious
choice. It would let me try using menus, windows, dialogs and simple graphics. After
the initial game was written, I added some other features just for the sake of making
the game a little more interesting. Then I added two text manipulation effects to take
some of the boredom out of the "About" dialog. The result is a program that is more
complicated than the average Tic Tac Toe program, but (hopefully) simple enough so
you can follow the code with relative ease and learn something from the techniques and
routines used in it.
The files used are "TicTacToe.asm", a resource file "TicTacToe_rsrc.asm" that
contains two icons used in the "About" dialog, two text effects routines "GrowText.asm
and "SnakeText.asm", and the link file "TicTacToe.link". I have found that building a
library of common subroutines (like GrowText and SnakeText) that can be linked
together and used in different programs is incredibly useful. After 5 years of 6502
programming on the Apple ][ without this feature, I have learned to love the Macintosh
Linker. The Tic Tac Toe program that follows could be split up into even more
subroutine files if desired.
The Program
The program starts off by initializing all routine manangers that it will be
using. I should point that there is a "resume" procedure pointer passed to _InitDialogs
(instead of the typical nil pointer). The consequence of this is that if a serious system
error should occur, the "resume" button will not be greyed and if you click on it
control will be passed to the "resume" procedure. In my case, all the resume procedure
does is jump to the Finder (via _exitToShell). I find this quite helpful when developing
a program because it is much quicker than rebooting the system. Of course, if the
program messed up RAM in some way it could be a big mistake to jump to the Finder and
continue work as if all was normal. But due to the nature of the program I was writing
(wasn't dealing with disk IO), I decided to use it anyway (and had no problems). Use
this with discretion and ALWAYS keep backups. If you're at all unsure (or nervous)
about the possible effects of your program, then don't use this method (use "CLR.L
-(SP)" in place of "PEA resume" to push a nil pointer. That will grey the "resume
button and force a restart if you get a serious system error). The next thing called is
_TEInit. As Inside Macintosh says, it should be called even though you may not use
TextEdit so that desk accessories and dialogs will work correctly.
Fig. 1 Our Tic Tac Toe Game!
After the basic initialization, all menus, windows and dialogs are created. The
reason for creating all of the windows and dialogs in the beginning of the program is
twofold. First, because it's nice having them all together in the source code for
debugging purporses. Secondly, and more important, it helps to keep the heap from
becoming fragmented if they are all created one after the other. This technique is
probably best used for static data structures that are needed throughout the entire
program (e.g. global variables). On larger programs with lots of windows and dialogs,
it is probably not a good idea to make them all at once (due to RAM limitations). The
remainder of the initialization and the main event loop (GetEvent) should be
straightforward. It is very similar to previous programs published in MacTutor.
The Dialogs
All of the dialogs use a routine called CenterString. Its function is to calculate the
offset from the left edge of a window needed to center a string in a window, given the
width of the window (the windowWidth variable) and a pointer to the string. The
algorithm is fairly obvious: subtract the length of the string (found from
_StringWidth) from windowWidth and divide by two. This value is returned in D0 and
is used as the horizontal value for the _MoveTo called just before _DrawString.
Additionally, the "About" dialog uses the two routines GrowText and SnakeText.
GrowText starts out by drawing the string at a textSize of 1 point and then redraws it at
2 point, etc., up to 12 point (erasing the previous string with _eraseRect just before
drawing the new string) so that it looks like the string is growing. SnakeText is rather
more complicated, but works in a similar manner. It doesn't really look like a snake,
but for lack of a better name that's what it is called. The best way to understand what it
is doing is to watch it perform a couple of times. Basically, it is redrawing characters
at different textSizes until they're all at 12 point. By studying the commented source
code, you should be able to get some idea of how it works. But you don't need to
understand how it works to use it within your own programs (just be sure you set up
the input registers correctly). I should mention that the rectangle that you pass a
pointer to must be tight fitting around the text it is to draw (i.e. no extra white space
around the top,left, or bottom -- the right side isn't too important). If it isn't, the
routine will still function, but it may not look too pretty on the screen (like a lot of
overdrawn or partially overdrawn characters). The method I used to find the correct
dimensions of the rectangle is to draw the string where I wanted it and then use
_FrameRect until I found the smallest possible rectangle that would enclose all of the
text. It will probably take a little trial and error to get the dimensions perfect, but it's
important.
F
ig. 2 With some nifty dialog 'about' boxes
The DefaultOutline subroutine is used to draw the 3 pixel wide oval around the
"OK" button in the dialogs in the standard way. It is worth pointing out that anything
you want in a dialog box that isn't in the dialog's item list has to be put in the dialog box
before calling _ModalDialog (when you call _ModalDialog, it draws the things that are
in the dialog's item list). So the oval around the "OK" button is actually drawn before
the button is drawn by _ModalDialog. This is also true for any dynamic text (or special
effects like GrowText or SnakeText) that you want to have in the dialogs.
Game Logic
Every time you select "New Game" from the "Game" menu, the computer
randomly decides who goes first. If you're looking at a blank board after you have
selected "New Game" then you go first, otherwise you'll see a square somewhere (the
computer's first move). The player is always circles, the computer is always squares.
The "Reverse Positions" option merely switches all circles for squares and squares for
circles -- it does not change the order of turns or who is what piece. It was included as
an option mainly because it was easy to code (besides, it does add a little to the interest
of the program). It is only enabled when a game is in progress.
If the difficulty is set on "easy" then the computer will always move randomly
(which gets boring after about 1 game). The "hard" difficulty should be called "not quite
so easy, but not very difficult either" but that is a bit of a long name for a menu option.
Face it, this is not a difficult game to play, there's not a whole lot you can do to make the
computer (or anyone else, for that matter) play more intelligently. When set on "hard
it makes three checks for intelligent moves before it decides to move randomly: (1)
checks if the computer can win in the next move, (2) checks if the computer can block
a player win in the next move, (3) checks if the middle square is taken. The "cheating
options were added for fun. As the dialog says, anyone who is allowed to cheat (player or
computer) is allowed to move on any square, even if someone else is already there. As
far as the coding of this is concerned, all it amounts to is bypassing the usual checks for
valid moves.
If there is a mouseDown event anywhere in the game window, the point is checked
to see if the player clicked in one of the nine squares. The coordinates of the click are
first converted into local coordinates by calling _GlobalToLocal and then checked to see
if the point is in any of the nine squares by calling _PtInRect. The coordinates of the
nine squares are in stored in the table BoardLocs. These coordinates are used as
dimensions for drawing the squares and circles (with _FrameRect and _FrameOval) as
well as checking for validity of mouseDown events.
Miscellaneous
The "Show/Hide scores" menu option was included to demonstrate how to change
menu items. All that is needed is to set up the parameters on the stack and call _SetItem.
Likewise, the checking and unchecking of the difficulty setting and cheat settings can be
accomplished with calls to _CheckItem.
One thing I learned while writing this program (after many headaches) is that
any variable declared with the "DS" (Define Storage) declarative will have to be
referenced with address register A5. This is not true for variables declared with the
"DC" (Define Constant) declarative. For instance, if ShowFlag is declared "ShowFlag DS
1" it will be referenced as "MOVE ShowFlag(A5),D0" but if it is declared "ShowFlag
DC 1" it will be referenced as "MOVE ShowFlag,D0". This is because all storage
declared with DS is put in the application globals space (pointed to by A5 when your
program starts) while all storage declared with DC stays within the program code
segment (addresses filled in by the Linker). If you use both types of declarations (DS
and DC) in your programs, as is done in this one, it is critical to pay attention to which
is which and use the correct type of reference or you will end up with random results.
As was mentioned above with respect to SnakeText and GrowText, the routines
presented here can be used as part of your own programs. The RandomBounds routine
(combined with its initialization of randSeed in the beginning) is a good way to get
bounded random numbers into your assembly language programs. It returns a positive
integer in the range 1 to D0, where you set D0 before calling it. The CenterString and
DefaultOutline routines should be useful, too.
; GrowText 4 March 1986
;--------------
; print a text string starting at 1 point up to 12 point,
; centered in a rectangle (horizontally, not vertically).
; the string gets a little bigger each time through the loop.
; inspired by Lynn Bond, who gets a little bigger each time
; she eats a pound of M&M's.
.TRAP _EraseRect $A8A3
.TRAP _TextSize $A88A
.TRAP _MoveTo $A893
.TRAP _DrawString $A884
Xref GrowText,CenterString, windowWidth
right EQU 6
left EQU 2
;------------
GrowText:
;------------
; before calling, user should push the following onto the stack:
; (in this order)
; address of string to print
; addr of rect that encloses the string
; vertical height to print string
;-------------
;save return address
MOVE.L (SP)+,returnAddress(A5)
;get parameters
MOVE (SP)+,vPosition(A5)
MOVE.L (SP)+,rectPointer(A5)
MOVE.L (SP)+,stringPointer(A5)
;save windowWidth
MOVE windowWidth(A5),-(SP)